package com.hmdp.service.impl;

import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.User;
import com.hmdp.entity.Voucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import lombok.RequiredArgsConstructor;
import org.apache.ibatis.javassist.compiler.ast.Variable;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@RequiredArgsConstructor
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    private final ISeckillVoucherService seckillVoucherService;

    private final RedisIdWorker redisIdWorker;

    /**
     * 秒杀抢券
     * @param voucherId
     * @return
     */
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1、查询优惠券
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        // 2、判断秒杀是否开始
        LocalDateTime beginTime = seckillVoucher.getBeginTime();
        LocalDateTime endTime = seckillVoucher.getEndTime();

        if (beginTime.isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀活动尚未开始！");
        }
        // 3、判断秒杀是否结束
        else if(endTime.isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀活动已经结束！");
        }
        // 4、判断库存是否充足
        if (seckillVoucher.getStock() < 1) {
            return Result.fail("库存不足，抢券失败！");
        }

        //   一、需要注意，虽然这里的代码看着平平无奇，
        // 但是思路和涉及的多线程知识是非常无敌的。
        // 首先是锁粒度，选择userId.toString().intern()作为锁对象，
        // 选择userId的原因是每一个用户的id都是唯一的，这样每个用户会拥有一个独立的锁。
        // 不同用户的请求可以并行执行，互不阻塞；
        // 而同一用户的请求会串行执行，避免并发问题。
        // 而抽取的方法中已经有了乐观锁保证线程安全，因此这里的同步代码块选择的对象锁完美，
        // 而为什么使用toString是因为即使 userId 数值相同，不同请求获取的 Long 对象实例也可能不同
        // 每次请求创建的每个对象是不一样的，需要转为字符串，
        // 但是转为字符串还是远远不够的，因为toString底层是new一个新的字符串，内容相同的字符串可能仍是不同实例
        // 要调用intern()，这个方法的作用是，如果在字符串池中有相同的字符串，直接指向字符串池中的字符串，
        // 这样就保证了相同的userId的所有请求竞争同一把锁
        // 不把synchronized关键字加在抽取的方法中就是以上原因
        // 还有不把这个同步代码块加在抽取的方法内部是因为方法有@Transactional事务
        // 加在方法体内会有并发安全问题：在事务没有提交前，同步代码块已经执行完毕并释放锁
        // 此时可能会有新的线程获取到锁，但是事务还没有提交，数据库中是没有生成订单的
        // 此时另一个获取到锁的线程就可能会出现数据库订单判断出错，同一个用户生成多个订单
        //   二、事务问题，抽取的方法中是有事务的，而调用的方法没有加事务，
        //   加上事务注解的事务功能是Spring使用AOP生成代理对象实现的
        //   直接返回createVoucherOrder(voucherId)是自调用，即this.createVoucherOrder(voucherId)
        //   而不是Spring生成的事务代理对象，事务是没有生效的,没有事务功能
        //   总体流程：获取锁->事务生效->是释放锁，总体需要注意释放锁的时机和事务生效问题
        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()) {
            IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public Result createVoucherOrder(Long voucherId) {
        //判断用户是否已经购买过了
        Long userId = UserHolder.getUser().getId();
        Integer count = lambdaQuery()
                .eq(VoucherOrder::getUserId, userId)
                .eq(VoucherOrder::getVoucherId, voucherId)
                .count();
        if (count > 0) {
            return Result.fail("用户已经购买过一次！");
        }
        // 5、扣减库存
        boolean update = seckillVoucherService.lambdaUpdate()
                .setSql("stock = stock - 1")
                .eq(SeckillVoucher::getVoucherId, voucherId)
                .gt(SeckillVoucher::getStock, 0)
                .update();
        if (!update) {
            return Result.fail("库存不足！");
        }
        // 6、创建订单
        long orderId = redisIdWorker.nextId("order");
        VoucherOrder voucherOrder = new VoucherOrder();
        voucherOrder.setId(orderId);
        voucherOrder.setVoucherId(voucherId);
        voucherOrder.setUserId(userId);
        voucherOrder.setStatus(1);
        voucherOrder.setCreateTime(LocalDateTime.now());
        save(voucherOrder);
        // 7、返回订单id
        return Result.ok(voucherId);
    }
}
